<!DOCTYPE html>
<html>
<head>
<title>DAY07</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
/* Author: Nicolas Hery - http://nicolashery.com */
/* Version: b13fe65ca28d2e568c6ed5d7f06581183df8f2ff */
/* Source: https://github.com/nicolahery/markdownpad-github */

/* RESET
=============================================================================*/

html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
  margin: 0;
  padding: 0;
  border: 0;
}

/* BODY
=============================================================================*/

body {
  font-family: Helvetica, arial, freesans, clean, sans-serif;
  font-size: 14px;
  line-height: 1.6;
  color: #333;
  background-color: #fff;
  padding: 20px;
  max-width: 960px;
  margin: 0 auto;
}

body>*:first-child {
  margin-top: 0 !important;
}

body>*:last-child {
  margin-bottom: 0 !important;
}

/* BLOCKS
=============================================================================*/

p, blockquote, ul, ol, dl, table, pre {
  margin: 15px 0;
}

/* HEADERS
=============================================================================*/

h1, h2, h3, h4, h5, h6 {
  margin: 20px 0 10px;
  padding: 0;
  font-weight: bold;
  -webkit-font-smoothing: antialiased;
}

h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
  font-size: inherit;
}

h1 {
  font-size: 28px;
  color: #000;
}

h2 {
  font-size: 24px;
  border-bottom: 1px solid #ccc;
  color: #000;
}

h3 {
  font-size: 18px;
}

h4 {
  font-size: 16px;
}

h5 {
  font-size: 14px;
}

h6 {
  color: #777;
  font-size: 14px;
}

body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
  margin-top: 0;
  padding-top: 0;
}

a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
  margin-top: 0;
  padding-top: 0;
}

h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
  margin-top: 10px;
}

/* LINKS
=============================================================================*/

a {
  color: #4183C4;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* LISTS
=============================================================================*/

ul, ol {
  padding-left: 30px;
}

ul li > :first-child, 
ol li > :first-child, 
ul li ul:first-of-type, 
ol li ol:first-of-type, 
ul li ol:first-of-type, 
ol li ul:first-of-type {
  margin-top: 0px;
}

ul ul, ul ol, ol ol, ol ul {
  margin-bottom: 0;
}

dl {
  padding: 0;
}

dl dt {
  font-size: 14px;
  font-weight: bold;
  font-style: italic;
  padding: 0;
  margin: 15px 0 5px;
}

dl dt:first-child {
  padding: 0;
}

dl dt>:first-child {
  margin-top: 0px;
}

dl dt>:last-child {
  margin-bottom: 0px;
}

dl dd {
  margin: 0 0 15px;
  padding: 0 15px;
}

dl dd>:first-child {
  margin-top: 0px;
}

dl dd>:last-child {
  margin-bottom: 0px;
}

/* CODE
=============================================================================*/

pre, code, tt {
  font-size: 12px;
  font-family: Consolas, "Liberation Mono", Courier, monospace;
}

code, tt {
  margin: 0 0px;
  padding: 0px 0px;
  white-space: nowrap;
  border: 1px solid #eaeaea;
  background-color: #f8f8f8;
  border-radius: 3px;
}

pre>code {
  margin: 0;
  padding: 0;
  white-space: pre;
  border: none;
  background: transparent;
}

pre {
  background-color: #f8f8f8;
  border: 1px solid #ccc;
  font-size: 13px;
  line-height: 19px;
  overflow: auto;
  padding: 6px 10px;
  border-radius: 3px;
}

pre code, pre tt {
  background-color: transparent;
  border: none;
}

kbd {
    -moz-border-bottom-colors: none;
    -moz-border-left-colors: none;
    -moz-border-right-colors: none;
    -moz-border-top-colors: none;
    background-color: #DDDDDD;
    background-image: linear-gradient(#F1F1F1, #DDDDDD);
    background-repeat: repeat-x;
    border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
    border-image: none;
    border-radius: 2px 2px 2px 2px;
    border-style: solid;
    border-width: 1px;
    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
    line-height: 10px;
    padding: 1px 4px;
}

/* QUOTES
=============================================================================*/

blockquote {
  border-left: 4px solid #DDD;
  padding: 0 15px;
  color: #777;
}

blockquote>:first-child {
  margin-top: 0px;
}

blockquote>:last-child {
  margin-bottom: 0px;
}

/* HORIZONTAL RULES
=============================================================================*/

hr {
  clear: both;
  margin: 15px 0;
  height: 0px;
  overflow: hidden;
  border: none;
  background: transparent;
  border-bottom: 4px solid #ddd;
  padding: 0;
}

/* TABLES
=============================================================================*/

table th {
  font-weight: bold;
}

table th, table td {
  border: 1px solid #ccc;
  padding: 6px 13px;
}

table tr {
  border-top: 1px solid #ccc;
  background-color: #fff;
}

table tr:nth-child(2n) {
  background-color: #f8f8f8;
}

/* IMAGES
=============================================================================*/

img {
  max-width: 100%
}
</style>
</head>
<body>
<h3>25. 收货地址-创建数据表</h3>
<pre><code>CREATE TABLE t_address (
    aid INT AUTO_INCREMENT COMMENT '收货地址id',
    uid INT COMMENT '用户id',
    name VARCHAR(50) COMMENT '收货人姓名',
    province_code CHAR(6) COMMENT '省-代号',
    province_name VARCHAR(50) COMMENT '省-名称',
    city_code CHAR(6) COMMENT '市-代号',
    city_name VARCHAR(50) COMMENT '市-名称',
    area_code CHAR(6) COMMENT '区-代号',
    area_name VARCHAR(50) COMMENT '区-名称',
    zip CHAR(6) COMMENT '邮编',
    address VARCHAR(100) COMMENT '详细地址',
    phone VARCHAR(20) COMMENT '手机',
    tel VARCHAR(20) COMMENT '固话',
    tag VARCHAR(20) COMMENT '地址类型',
    is_default INT COMMENT '是否默认：0-非默认，1-默认',
    created_user VARCHAR(20) COMMENT '创建人',
    created_time DATETIME COMMENT '创建时间',
    modified_user VARCHAR(20) COMMENT '最后修改人',
    modified_time DATETIME COMMENT '最后修改时间',
    PRIMARY KEY (aid)
) DEFAULT CHARSET=UTF8;
</code></pre>

<h3>26. 收货地址-创建实体类</h3>
<p>创建<code>cn.tedu.store.entity.Address</code>继承自<code>BaseEntity</code>。</p>
<h3>27. 收货地址-增加-持久层</h3>
<p><strong>a. 规划SQL语句</strong></p>
<p>增加收货地址的SQL语句应该是：</p>
<pre><code>insert into t_address (除了aid以外的所有字段) values(匹配的实体类的属性值);
</code></pre>

<p>在增加时，还需要考虑当前增加的收货地址是不是当前用户的默认收货地址，可以设定规则“用户的第1条收货地址是默认的，后续增加的都不是默认的”，则可以查询该用户的收货地址数量，如果为0，则即将增加的就是第1条，就应该是默认的，如果不为0，表示用户已经有一些收货地址数据了，即将增加的不是第1条，则应该是非默认的！所以，需要“查询该用户的收货地址数量”的查询功能，对应的SQL语句应该是：</p>
<pre><code>select count(*) from t_address where uid=?
</code></pre>

<p><strong>b. 接口与抽象方法</strong></p>
<p>创建<code>cn.tedu.store.mapper.AddressMapper</code>接口，并添加抽象方法：</p>
<pre><code>Integer insert(Address address);

Integer countByUid(Integer uid);
</code></pre>

<p><strong>c. 配置映射</strong></p>
<p>复制得到<code>AddressMapper.xml</code>文件，确定根节点的<code>namespace</code>对应以上接口，然后，配置以上2个抽象方法的映射：</p>
<pre><code>&lt;mapper namespace=&quot;cn.tedu.store.mapper.AddressMapper&quot;&gt;

    &lt;!-- 插入收货地址数据 --&gt;
    &lt;!-- Integer insert(Address address) --&gt;
    &lt;insert id=&quot;insert&quot;
        useGeneratedKeys=&quot;true&quot;
        keyProperty=&quot;aid&quot;&gt;
        INSERT INTO t_address (
            uid, name,
            province_code, province_name,
            city_code, city_name,
            area_code, area_name,
            zip, address,
            phone, tel,
            tag, is_default,
            created_user, created_time,
            modified_user, modified_time
        ) VALUES (
            #{uid}, #{name},
            #{provinceCode}, #{provinceName},
            #{cityCode}, #{cityName},
            #{areaCode}, #{areaName},
            #{zip}, #{address},
            #{phone}, #{tel},
            #{tag}, #{isDefault},
            #{createdUser}, #{createdTime},
            #{modifiedUser}, #{modifiedTime}
        )
    &lt;/insert&gt;

    &lt;!-- 统计某个用户的收货地址数据的数量 --&gt;
    &lt;!-- Integer countByUid(Integer uid) --&gt;
    &lt;select id=&quot;countByUid&quot;
        resultType=&quot;java.lang.Integer&quot;&gt;
        SELECT 
            COUNT(*) 
        FROM 
            t_address 
        WHERE 
            uid=#{uid}
    &lt;/select&gt;

&lt;/mapper&gt;
</code></pre>

<p>在<strong>src/test/java</strong>下创建<code>AddressMapperTests</code>单元测试类，添加测试相关注解，然后，编写并执行以上2个功能的单元测试：</p>
<pre><code>@RunWith(SpringRunner.class)
@SpringBootTest
public class AddressMapperTests {

    @Autowired
    private AddressMapper mapper;

    @Test
    public void insert() {
        Address address = new Address();
        address.setUid(1);
        address.setName(&quot;赵六&quot;);
        Integer rows = mapper.insert(address);
        System.err.println(&quot;rows=&quot; + rows);
        System.err.println(&quot;id=&quot; + address.getAid());
    }

    @Test
    public void countByUid() {
        Integer uid = 10;
        Integer count = mapper.countByUid(uid);
        System.err.println(&quot;count=&quot; + count);
    }

}
</code></pre>

<h3>28. 收货地址-增加-业务层</h3>
<p><strong>a. 规划异常</strong></p>
<p>增加收货地址会执行INSERT操作，则可能抛出<code>InsertException</code>。</p>
<p>还可以设定规则，当收货地址的数量达到指定值后，不允许再创建，即每个用户最多只可以有X条收货地址，如果用户创建的收货地址达到上限后仍尝试继续创建，则可以抛出：<code>AddressCountLimitException</code>。</p>
<p>则需要创建<code>cn.tedu.store.service.ex.AddressCountLimitException</code>。</p>
<p><strong>b. 接口与抽象方法</strong></p>
<p>创建<code>cn.tedu.store.service.IAddressService</code>业务层接口，并添加抽象方法：</p>
<pre><code>void addnew(Address address, Integer uid, String username) throws AddressCountLimitException, InsertException;
</code></pre>

<p>并在接口中声明：</p>
<pre><code>int ADDRESS_MAX_COUNT = 20;
</code></pre>

<p><strong>c. 实现类与重写方法</strong></p>
<p>创建<code>cn.tedu.store.service.impl.AddressServiceImpl</code>业务层实现类，实现以上接口，添加<code>@Service</code>注解，并在类中添加持久层<code>@Autowired private AddressMapper addressMppaer;</code>接口的对象：</p>
<pre><code>@Service
public class AddressServiceImpl implements IAddressService {

    @Autowired
    private AddressMapper addressMppaer;

    public void addnew(Address address, Integer uid, String username) throws AddressCountLimitException, InsertException {
        // TODO 

    }
}
</code></pre>

<p>在持久层中已经完成的增、删、改、查相关方法，每一个方法都有可能出现在多个业务中！在业务层调用时，可能都需要经过某些验证才可以保证正常使用，而多次调用，不应该多次编写验证的代码或复制这些代码，而应该把验证和持久层的功能封装在业务层中的某个方法中，以便于后续统一使用，例如，在业务层实现类中：</p>
<pre><code>private void insert(Address address) {
    Integer rows = addressMapper.insert(address);
    if (rows != 1) {
        throw new InsertException(&quot;增加收货地址失败！插入数据时出现未知错误！&quot;);
    }
}

private Integer countByUid(Integer uid) {
    Integer count = addressMapper.countByUid(uid);
    return count;
}
</code></pre>

<p>所以，总的看来，在业务层实现类中，会把持久层接口中已有方法都添加一个同名的、私有的方法，并调用持久层对象来实现相关功能，添加相关验证和判断。</p>
<p>后续，在业务层的公有方法(重写的接口中的方法)中，都不会直接调用持久层对象来实现某种操作，而是调用自身的私有方法来实现。</p>
<p>然后，重写接口中的抽象方法：</p>
<pre><code>public void addnew(Address address, Integer uid, String username) throws AddressCountLimitException, InsertException {
    // 根据参数uid查询当前用户的收货地址数量
    // 判断收货地址数量是否达到上限值ADDRESS_MAX_COUNT
    // 是：抛出：AddressCountLimitException

    // 补全数据：uid

    // TODO 补全数据：province_name, city_name, area_name

    // 判断当前用户的收货地址数量是否为0，并决定is_default的值
    // 补全数据：is_default

    // 创建当前时间对象
    // 补全数据：4个日志

    // 插入收货地址数据
}
</code></pre>

<p>具体实现为：</p>
<pre><code>@Override
public void addnew(Address address, Integer uid, String username)
        throws AddressCountLimitException, InsertException {
    // 根据参数uid查询当前用户的收货地址数量
    Integer count = countByUid(uid);
    // 判断收货地址数量是否达到上限值ADDRESS_MAX_COUNT
    if (count &gt;= ADDRESS_MAX_COUNT) {
        // 是：抛出：AddressCountLimitException
        throw new AddressCountLimitException(
            &quot;增加收货地址失败！当前收货地址数量已经达到上限！最多允许创建&quot; + ADDRESS_MAX_COUNT + &quot;条，已经创建&quot; + count + &quot;条！&quot;);
    }

    // 补全数据：uid
    address.setUid(uid);

    // TODO 补全数据：province_name, city_name, area_name

    // 判断当前用户的收货地址数量是否为0，并决定is_default的值
    Integer isDefault = count == 0 ? 1 : 0;
    // 补全数据：is_default
    address.setIsDefault(isDefault);

    // 创建当前时间对象
    Date now = new Date();
    // 补全数据：4个日志
    address.setCreatedUser(username);
    address.setCreatedTime(now);
    address.setModifiedUser(username);
    address.setModifiedTime(now);

    // 插入收货地址数据
    insert(address);
}
</code></pre>

<p>最后，在<strong>src/test/java</strong>下创建<code>cn.tedu.store.service.AddressServiceTests</code>测试类，编写并执行单元测试：</p>
<pre><code>@RunWith(SpringRunner.class)
@SpringBootTest
public class AddressServiceTests {

    @Autowired
    IAddressService service;

    @Test
    public void addnew() {
        try {
            Integer uid = 2;
            String username = &quot;系统管理员&quot;;
            Address address = new Address();
            address.setName(&quot;小孙同学&quot;);
            service.addnew(address, uid, username);
            System.err.println(&quot;OK.&quot;);
        } catch (ServiceException e) {
            System.err.println(e.getClass().getName());
            System.err.println(e.getMessage());
        }
    }

}
</code></pre>

<h3>29. 收货地址-增加-控制器层</h3>
<p><strong>a. 处理异常</strong></p>
<p>此次需要处理<code>AddressCountLimitException</code>。</p>
<p><strong>b. 设计请求</strong></p>
<pre><code>请求路径：/addresses/addnew
请求参数：Address address, HttpSession session
请求类型：POST
响应数据：JsonResult&lt;Void&gt;
</code></pre>

<p><strong>c. 处理请求</strong></p>
<p>创建<code>cn.tedu.store.controller.AddressController</code>控制器类，继承自<code>BaseController</code>，添加<code>@RestController</code>和<code>@RequestMapping(&quot;addresses&quot;)</code>注解，并在类中添加业务层<code>@Autowired private IAddressService addressService;</code>接口的对象：</p>
<pre><code>@RestController
@RequestMapping(&quot;addresses&quot;)
public class AddressController extends BaseController {

    @Autowired 
    private IAddressService addressService;

}
</code></pre>

<p>然后，在类中添加处理请求的方法：</p>
<pre><code>@RequestMapping(&quot;addnew&quot;)
public JsonResult&lt;Void&gt; addnew(
    Address address, HttpSession session) {
    // 从Session中获取uid和username
    // 调用业务层对象执行增加
    // 响应成功
}
</code></pre>

<p>完成后，打开浏览器，先登录，然后可以通过<code>http://localhost:8080/addresses/addnew?name=David</code>进行测试。</p>
<h3>30. 收货地址-增加-前端页面</h3>
<h3>31. 省市区数据-导入数据</h3>
<p>登录mysql控制台，通过以下指令导入：</p>
<pre><code>source 文件路径
</code></pre>

<h3>32. 创建实体类</h3>
<p>创建<code>cn.tedu.store.entity.District</code>实体类，由于字典表的数据不会被修改，所以，表中并没有4个日志字段，则实体类中也不需要，则该实体类不需要继承自<code>BaseEntity</code>，但是，仍需要实现<code>Serializable</code>接口：</p>
<pre><code>/**
 * 省/市/区数据的实体类
 */
public class District implements Serializable {

    private static final long serialVersionUID = 3674364250490127944L;

    private Integer id;
    private String parent;
    private String code;
    private String name;

    // ...
}
</code></pre>

<h3>33. 省市区-获取列表-持久层</h3>
<p><strong>a. 规划SQL语句</strong></p>
<p>在<code>t_dict_district</code>中记录了全国所有的省、市、区的数据，如果需要查询全国所有的省，或某个省所有市，或某个市所有区，需要执行的SQL语句是一样的，只不过参数不同而已：</p>
<pre><code>select * from t_dict_district where parent=?
</code></pre>

<p><strong>b. 接口与抽象方法</strong></p>
<p>创建<code>cn.tedu.store.mapper.DistrictMapper</code>接口，然后指定抽象方法：</p>
<pre><code>List&lt;District&gt; findByParent(String parent);
</code></pre>

<p><strong>c. 配置映射</strong></p>
<p>复制得到<code>DistrictMapper.xml</code>文件，并配置以上抽象方法的映射：</p>
<pre><code>&lt;mapper namespace=&quot;cn.tedu.store.mapper.DistrictMapper&quot;&gt;

    &lt;!-- 根据父级代号获取全国所有省/某省所有市/某市所有区的列表 --&gt;
    &lt;!-- List&lt;District&gt; findByParent(String parent) --&gt;
    &lt;select id=&quot;findByParent&quot;
        resultType=&quot;cn.tedu.store.entity.District&quot;&gt;
        SELECT
            id, parent,
            code, name
        FROM
            t_dict_district
        WHERE
            parent=#{parent}
        ORDER BY
            id
    &lt;/select&gt;

&lt;/mapper&gt;
</code></pre>

<p><strong>注意：如果查询的结果中包含多条数据，一定要显式的指定排序规则，如果没有指定，则实际获取的数据将是没有顺序的（可能绝大部分时候会表现为按照id排序，事实上是根本没有顺序的）！</strong></p>
<p>然后，编写并执行单元测试：</p>
<pre><code>@RunWith(SpringRunner.class)
@SpringBootTest
public class DistrictMapperTests {

    @Autowired
    private DistrictMapper mapper;

    @Test
    public void findByParent() {
        String parent = &quot;320100&quot;;
        List&lt;District&gt; list = mapper.findByParent(parent);
        System.err.println(&quot;BEGIN:&quot;);
        for (District item : list) {
            System.err.println(item);
        }
        System.err.println(&quot;END.&quot;);
    }

}
</code></pre>

<h3>34. 省市区-获取列表-业务层</h3>
<p><strong>a. 规划异常</strong></p>
<p>无</p>
<p><strong>b. 接口与抽象方法</strong></p>
<p>创建<code>cn.tedu.store.service.IDistrictService</code>接口，并添加抽象方法：</p>
<pre><code>/**
 * 根据父级代号获取全国所有省/某省所有市/某市所有区的列表
 * @param parent 父级代号，如果尝试获取全国所有省，则代号应该使用&quot;86&quot;
 * @return 全国所有省/某省所有市/某市所有区的列表
 */
List&lt;District&gt; getByParent(String parent);
</code></pre>

<p><strong>c. 实现类与重写方法</strong></p>
<p>创建<code>cn.tedu.store.service.impl.DistrictServiceImpl</code>业务层实现类，实现以上接口，添加<code>@Service</code>注解，并在类中添加持久层<code>@Autowired private DistrictMapper districtMppaer;</code>接口的对象。</p>
<p>复制<code>DistrictMapper</code>接口中的抽象方法，并私有化实现它：</p>
<pre><code>/**
 * 根据父级代号获取全国所有省/某省所有市/某市所有区的列表
 * @param parent 父级代号，如果尝试获取全国所有省，则代号应该使用&quot;86&quot;
 * @return 全国所有省/某省所有市/某市所有区的列表
 */
private List&lt;District&gt; findByParent(String parent) {
    return districtMapper.findByParent(parent);
}
</code></pre>

<p>然后，重写接口中定义的抽象方法：</p>
<pre><code>public List&lt;District&gt; getByParent(String parent) {
    return findByParent(parent);
}
</code></pre>

<p>最后，编写并执行单元测试：</p>
<pre><code>@RunWith(SpringRunner.class)
@SpringBootTest
public class DistrictServiceTests {

    @Autowired
    private IDistrictService service;

    @Test
    public void getByParent() {
        String parent = &quot;86&quot;;
        List&lt;District&gt; list = service.getByParent(parent);
        System.err.println(&quot;BEGIN:&quot;);
        for (District item : list) {
            System.err.println(item);
        }
        System.err.println(&quot;END.&quot;);
    }

}
</code></pre>

<h3>35. 省市区-获取列表-控制器层</h3>
<p><strong>a. 处理异常</strong></p>
<p>无</p>
<p><strong>b. 设计请求</strong></p>
<pre><code>请求路径：/districts/
请求参数：String parent
请求类型：GET
响应数据：JsonResult&lt;List&lt;District&gt;&gt;
是否拦截：否，不拦截，需要在登录拦截器的配置中添加白名单
</code></pre>

<p><strong>c. 处理请求</strong></p>
<p>创建<code>cn.tedu.store.controller.DistrictController</code>，继承自<code>BaseController</code>，添加<code>@RestController</code>和<code>@RequestMapping(&quot;districts&quot;)</code>注解，在类中声明<code>@Autowired private IDistrictService districtService;</code>业务层对象。</p>
<p>然后，在控制器类添加处理请求的方法：</p>
<pre><code>@RestController
@RequestMapping(&quot;districts&quot;)
public class DistrictController extends BaseController {

    @Autowired
    private IDistrictService districtService;

    @GetMapping(&quot;/&quot;)
    public JsonResult&lt;List&lt;District&gt;&gt; getByParent(String parent) {
        List&lt;District&gt; data = districtService.getByParent(parent);
        return new JsonResult&lt;List&lt;District&gt;&gt;(SUCCESS, data);
    }

}
</code></pre>

<p>完成后，打开浏览器，输入<code>http://localhost:8080/districts/?parent=86</code>进行测试。</p>

</body>
</html>
<!-- This document was created with MarkdownPad, the Markdown editor for Windows (http://markdownpad.com) -->
